package server;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;
import messages.ClientServerChatMessage;
import messages.ServerParameters;
import messages.ServerServerMessage;
import server.client.ClientThread;
import server.client.ClientThreadIF;
import server.client.ServerClientIF;
import server.gui.GUIServerIF;
import server.gui.ServerGUIIF;
import server.server.InThread;
import server.server.OutThread;
import server.server.ServerInIF;
import server.server.ServerOutIF;

public class Server implements ServerClientIF, ServerGUIIF, ServerInIF, ServerOutIF {
	public static int uniqueId;
	private ArrayList<ClientThreadIF> clietThread;
	private ArrayList<InThread> inServersThreads;
	private ArrayList<ServerParameters> myServerList;
	private ServerParameters myServerParameters;
	private GUIServerIF serverGUI;
	private SimpleDateFormat simpleDateFormat;
	private int port;
	private String localAddress = "localhost";
	private int outServerPort = 0;
	private String outServerAddress = null;
	private boolean keepGoing;
	private boolean connectToAnotherServer = false; // alone or first server
	private boolean aloneServer = true;
	private InThread inServerThread;
	private OutThread outServerThread = null;
	private final static String CLIENT = "client";
	public final static String SERVER = "server";
	private int minNumberOfClient = -1;
	private int maxNumberOfClient = -1;
	private ArrayList<String> serverLauncher;
	private HashMap<String, String> hm;
	private ServerSocket serverSocket;

	public Server(int port) {
		this(port, (GUIServerIF) null);
	}

	public Server(int port, String localAddress) {
		this(port, null, localAddress);
	}

	public Server(int port, String localAddress, int maxNumberOfClient) {
		this(port, null, localAddress, maxNumberOfClient);
	}

	public Server(int port, String localAddress, int outServerPort,
			String outServerAddress) {
		this(port, null, localAddress, outServerPort, outServerAddress);
	}

	public Server(int port, String localAddress, int outServerPort,
			String outServerAddress, int maxNumberOfClient) {
		this(port, null, localAddress, outServerPort, outServerAddress, maxNumberOfClient);
		this.maxNumberOfClient = maxNumberOfClient;
	}

	public Server(int port, String localAddress, int outServerPort,
			String outServerAddress, int maxNumberOfClient, int minNumberOfClient) {
		this(port, null, localAddress, outServerPort, outServerAddress,
				maxNumberOfClient, minNumberOfClient);
		this.minNumberOfClient = minNumberOfClient;
		this.maxNumberOfClient = maxNumberOfClient;
	}

	public Server(int port, GUIServerIF serverGUI) {
		this.serverGUI = serverGUI;
		this.port = port;
		simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS");
		clietThread = new ArrayList<ClientThreadIF>();
		inServersThreads = new ArrayList<InThread>();
		this.localAddress = "localhost";
		myServerParameters = new ServerParameters(this.localAddress, port);
		// TODO if first Server ********
		myServerList = new ArrayList<ServerParameters>();
		myServerList.add(myServerParameters);
	}

	public Server(int port, GUIServerIF serverGUI, String localAddress) {
		this(port, serverGUI);
		this.localAddress = localAddress;
	}

	public Server(int port, GUIServerIF serverGUI, String localAddress,
			int maxNumberOfClient) {
		this(port, serverGUI, localAddress);
		this.maxNumberOfClient = maxNumberOfClient;
		initServerLauncher();
	}

	public Server(int port, GUIServerIF serverGUI, String localAddress,
			int outServerPort, String outServerAddress) {
		this(port, serverGUI, localAddress);
		this.outServerAddress = outServerAddress;
		this.outServerPort = outServerPort;
		connectToAnotherServer = true;
		this.aloneServer = false;
	}

	public Server(int port, GUIServerIF serverGUI, String localAddress,
			int outServerPort, String outServerAddress, int maxNumberOfClient) {
		this(port, serverGUI, localAddress, outServerPort, outServerAddress);
		initServerLauncher();
	}

	public Server(int port, GUIServerIF serverGUI, String localAddress,
			int outServerPort, String outServerAddress, int maxNumberOfClient,
			int minNumberOfClient) {
		this(port, serverGUI, localAddress, outServerPort, outServerAddress,
				maxNumberOfClient);
	}

	private void initServerLauncher() {
		Properties prop = new Properties();
		serverLauncher = new ArrayList<String>();
		hm = new HashMap<String, String>();
		try {
			prop.load(new FileInputStream("../config.properties"));
			Enumeration<Object> em = prop.keys();
			while (em.hasMoreElements()) {
				String key = (String) em.nextElement();
				int idx = key.indexOf("_");
				String value = (String) prop.get(key);
				value = value.replace("_", " ");
				serverLauncher.add(key.substring(0, idx) + ":"
						+ Integer.parseInt(key.substring(idx + 1)));
				hm.put(key.substring(0, idx) + ":"
						+ Integer.parseInt(key.substring(idx + 1)), value);
			}
		} catch (IOException ex) {
		}

		for (String key : hm.keySet()) {
			System.out.println(key + " " + hm.get(key));
		}
	}

	public void start() {
		System.out.println(maxNumberOfClient + " " + minNumberOfClient);
		keepGoing = true;
		try {
			if (connectToAnotherServer) {
				connectToAnotherServer(outServerAddress, outServerPort, false);
			}
			serverSocket = new ServerSocket();
			serverSocket.bind(new InetSocketAddress(localAddress, port));
			while (keepGoing) {
				display("Server waiting for Clients/Servers on " + localAddress + ":"
						+ port + ".");
				Socket socket = serverSocket.accept();
				if (!keepGoing)
					break;
				ObjectInputStream sInput = new ObjectInputStream(socket.getInputStream());
				String init = (String) sInput.readObject();
				if (init.equalsIgnoreCase(CLIENT)) {
					ClientThreadIF t = new ClientThread(socket, this, ++uniqueId, sInput);
					if (t.allowCoonection()) {
						clietThread.add(t);
						t.start();
						this.setSSMsg(new ServerServerMessage(
								new ClientServerChatMessage(
										ClientServerChatMessage.MESSAGE, t.getUsername()
												+ " connected"), " ::<======>::",
								getMyServerParameters()));
						this.incNumberOfClients();

						if (maxNumberOfClient - clietThread.size() == 0) {
							startServer();
						}
					} else {
						t.disconnect();
					}
				} else if (init.equalsIgnoreCase(SERVER)) {
					inServerThread = new InThread(socket, sInput, this);
					inServersThreads.add(inServerThread);
					inServerThread.start();
				}
			}
			try {
				serverSocket.close();
				for (int i = 0; i < clietThread.size(); ++i) {
					ClientThreadIF tc = clietThread.get(i);
					tc.disconnect();
				}
			} catch (Exception e) {
				display("Exception closing the server and clients: " + e);
			}
		} catch (IOException e) {
			String msg = simpleDateFormat.format(new Date())
					+ " Exception on new ServerSocket: " + e + "\n";
			display(msg);
		} catch (ClassNotFoundException e) {
		}
	}

	public String getServerStartString() throws Exception {
		if (serverLauncher.size() < 1) {
			return "";
		}
		for (String string : hm.keySet()) {
			int idx = string.indexOf(":");
			if (!serverExist(new ServerParameters(string.substring(0, idx),
					Integer.parseInt(string.substring(idx + 1))))) {
				return string.substring(0, idx) + ":"
						+ Integer.parseInt(string.substring(idx + 1)) + " "
						+ localAddress + ":" + port + " " + hm.get(string);
			}
		}

		return "";
	}

	public boolean startServer() {

		try {
			String string = getServerStartString();
			ProcessBuilder pb;
			if (string.length() > 2) {
				System.out.println("#################provo ad avviare un altro server "
						+ string);
				pb = new ProcessBuilder("/bin/bash", "./startServer.sh", string);
			} else {
				return false;
			}
			pb.start();
		} catch (Exception er) {
			display("Erorre durante avvio un nuovo server: " + er.toString());
			return false;
		}
		return true;
	}

	public synchronized boolean connectToAnotherServer(String outServerAddress,
			int outServerPort, boolean reconnect) {
		outServerThread = new OutThread(outServerAddress, outServerPort, this, reconnect);
		outServerThread.start();
		return true;
	}

	public void incNumberOfClients() {
		this.myServerParameters.incNumberOfClients();
		ArrayList<ServerParameters> tmp = new ArrayList<ServerParameters>();
		for (ServerParameters parameters : myServerList) {
			if (equalServersAddressPort(parameters, myServerParameters)) {
				tmp.add(new ServerParameters(myServerParameters.getAddress(),
						myServerParameters.getPort(), myServerParameters
								.getNumberOfClients()));
			} else {
				tmp.add(parameters);
			}
		}
		this.setMyServerList(tmp);
		setSSMsg(new ServerServerMessage(getMyServerList(), getMyServerParameters()));
	}

	public void decNumberOfClients() {
		this.myServerParameters.decNumberOfClients();
		ArrayList<ServerParameters> tmp = new ArrayList<ServerParameters>();
		for (ServerParameters parameters : myServerList) {
			if (equalServersAddressPort(parameters, myServerParameters)) {
				tmp.add(new ServerParameters(myServerParameters.getAddress(),
						myServerParameters.getPort(), myServerParameters
								.getNumberOfClients()));

			} else {
				tmp.add(parameters);
			}
		}
		this.setMyServerList(tmp);
		setSSMsg(new ServerServerMessage(myServerList, myServerParameters));
	}

	public ArrayList<ClientThreadIF> getClietThread() {
		return clietThread;
	}

	public SimpleDateFormat getSimpleDateFormat() {
		return simpleDateFormat;
	}

	public void stop() {
		keepGoing = false;
		outServerThread.disconnect();
		for (ClientThreadIF client : clietThread) {
			client.disconnect();
		}
		for (InThread serverInIF : inServersThreads) {
			serverInIF.disconnect();
		}

		outServerThread.disconnect();
		try {
			serverSocket.close();
		} catch (Exception e) {
			System.exit(0);
		}
		System.exit(0);
	}

	public void display(String msg) {
		String time = simpleDateFormat.format(new Date()) + " " + msg;
		if (serverGUI == null)
			System.out.println(time);
		else
			serverGUI.appendEvent(time + "\n");
	}

	public synchronized void broadcast(String message) {
		String time = simpleDateFormat.format(new Date());
		String messageLf = time + " " + message + "\n";
		if (serverGUI == null)
			System.out.print(messageLf);
		else
			serverGUI.appendRoom(messageLf);
		for (int i = clietThread.size(); --i >= 0;) {
			ClientThreadIF ct = clietThread.get(i);
			if (!ct.writeMsg(messageLf)) {
				this.setSSMsg(new ServerServerMessage(new ClientServerChatMessage(
						ClientServerChatMessage.MESSAGE, clietThread.get(i).getUsername()
								+ " disconnected"), " ::<======>::",
						getMyServerParameters()));
				clietThread.remove(i);
				this.decNumberOfClients();
				display("Disconnected Clieknt " + ct.getUsername()
						+ " removed from list.");
				halt();
			}
		}
	}

	public synchronized void broadcast(ArrayList<ServerParameters> message) {

		for (int i = clietThread.size(); --i >= 0;) {
			ClientThreadIF ct = clietThread.get(i);
			if (!ct.writeMsg(message)) {
				this.setSSMsg(new ServerServerMessage(new ClientServerChatMessage(
						ClientServerChatMessage.MESSAGE, clietThread.get(i).getUsername()
								+ " disconnected"), " ::<======>::",
						getMyServerParameters()));
				clietThread.remove(i);
				this.decNumberOfClients();
				display("Disconnected Client " + ct.getUsername() + " removed from list.");
				halt();

			}
		}
	}

	public synchronized void remove(int id) {
		for (int i = 0; i < clietThread.size(); ++i) {
			ClientThreadIF ct = clietThread.get(i);
			if (ct.getId() == id) {
				this.setSSMsg(new ServerServerMessage(new ClientServerChatMessage(
						ClientServerChatMessage.MESSAGE, clietThread.get(i).getUsername()
								+ " disconnected"), " ::<======>:",
						getMyServerParameters()));
				clietThread.remove(i);
				this.decNumberOfClients();
				halt();
				return;
			}
		}
	}

	private void halt() {
		if (clietThread.size() <= minNumberOfClient) {
			if (myServerList.size() > 1) {
				stop();
			}
		}
	}

	public boolean isAloneServer() {
		return aloneServer;
	}

	public void setAloneServer(boolean aloneServer) {
		this.aloneServer = aloneServer;
	}

	public synchronized ArrayList<ServerParameters> getMyServerList() {
		return this.myServerList;
	}

	public synchronized void setMyServerList(ArrayList<ServerParameters> myServerList) {
		ArrayList<ServerParameters> tmpList = new ArrayList<ServerParameters>();
		for (ServerParameters serverParameters : myServerList) {
			ServerParameters tmpParameters = new ServerParameters(
					serverParameters.getAddress(), serverParameters.getPort(),
					serverParameters.getNumberOfClients());
			tmpParameters.setRingNumber(serverParameters.getRingNumber());
			tmpList.add(tmpParameters);

		}
		this.myServerList = tmpList;
		for (ServerParameters serverParameters : myServerList) {
			if (equalServersAddressPort(serverParameters, myServerParameters)) {
				this.myServerParameters = serverParameters;
				break;
			}
		}
		if (getMyServerList().size() == 1) {
			outServerAddress = null;
			outServerPort = -1;
			aloneServer = true;
		} else {
			outServerAddress = myOutputServer().getAddress();
			outServerPort = myOutputServer().getPort();
		}
		String msg = "#RT# ";
		for (ServerParameters serverParameters : myServerList) {
			msg = msg + serverParameters.getAddress() + ":" + serverParameters.getPort()
					+ "<-";
		}
		display(msg);
	}

	public void addInServer(ServerParameters InServer) {
		ArrayList<ServerParameters> tempServersList = new ArrayList<ServerParameters>();
		if (serverExist(InServer)) {
			tempServersList = this.getMyServerList();
		} else {
			int i = 0;
			for (ServerParameters serverParameters : myServerList) {
				if (equalServersAddressPort(serverParameters, myServerParameters)) {
					serverParameters.setRingNumber(i);
					myServerParameters = serverParameters;
					tempServersList.add(serverParameters);
					i++;
					InServer.setRingNumber(i);
					tempServersList.add(InServer);
					i++;
				} else {
					serverParameters.setRingNumber(i);
					tempServersList.add(serverParameters);
					i++;
				}
			}
		}
		this.setMyServerList(tempServersList);
	}

	private boolean serverExist(ServerParameters InServer) {
		for (ServerParameters serverParameters : this.getMyServerList()) {
			if (equalServersAddressPort(serverParameters, InServer)) {
				return true;
			}
		}
		return false;
	}

	private boolean equalServersAddressPort(ServerParameters parameters,
			ServerParameters parameters2) {
		if (parameters.getAddress().equalsIgnoreCase(parameters2.getAddress())
				&& parameters.getPort() == parameters2.getPort()) {
			return true;
		}
		return false;
	}

	public ServerParameters getMyServerParameters() {
		return myServerParameters;
	}

	public synchronized void setSSMsg(ServerServerMessage ssMsg) {
		if (ssMsg.getMessageType() == ServerServerMessage.SERVERLIST) {
			if (aloneServer && myServerList.size() > 1) {
				aloneServer = !aloneServer;
				ServerParameters outSerParameters = myOutputServer();
				outServerAddress = outSerParameters.getAddress();
				outServerPort = outSerParameters.getPort();
				connectToAnotherServer(outServerAddress, outServerPort, false);
			} else {
				if (ssMsg.getCounter() >= myServerList.size()) {

				} else {
					// 0 uguali 2-input 1-output 3-clienti
					// this.display("0-uguali;2-input;1-output;3-clienti "
					// + isEqualServerList(ssMsg.getServerList()));
					switch (isEqualServerList(ssMsg.getServerList())) {
					case -1:
						setMyServerList(ssMsg.getServerList());
						outServerThread.writeMsg(ssMsg);
						broadcast(getMyServerList());
						break;
					case 0:
						if (outServerThread != null) {
							outServerThread.writeMsg(ssMsg);
						}
//						broadcast(message)
						break;
					case 1:
						setMyServerList(ssMsg.getServerList());
						broadcast(getMyServerList());
						if (outServerThread != null) {
							outServerThread.disconnect();
							outServerThread = null;
						}
						this.connectToAnotherServer(outServerAddress, outServerPort,
								false);
						break;
					case 2:
						setMyServerList(ssMsg.getServerList());
						if (outServerThread != null) {
							outServerThread.writeMsg(ssMsg);
						}
						broadcast(getMyServerList());
						break;
					case 3:
						if (outServerThread != null) {
							outServerThread.writeMsg(ssMsg);
						}
						this.setMyServerList(ssMsg.getServerList());
						for (int i = 0; i < ssMsg.getServerList().size(); i++) {
							if (ssMsg.getServerList().get(i).getNumberOfClients() != getMyServerList()
									.get(i).getNumberOfClients()) {
								getMyServerList().get(i)
										.setNumberOfClients(
												ssMsg.getServerList().get(i)
														.getNumberOfClients());
							}
						}
						broadcast(getMyServerList());
						break;
					default:
						break;
					}
				}
			}
		} else if (ssMsg.getMessageType() == ServerServerMessage.MESSAGE) {
			if (ssMsg.getCounter() >= myServerList.size()) {
			} else {
				String message = ssMsg.getParameters().getAddress() + "/"
						+ ssMsg.getParameters().getPort() + "::" + ssMsg.getNick() + ": "
						+ ssMsg.getClientServerChatMessage().getMessage();
				broadcast(message);
				if (outServerThread != null) {
					outServerThread.writeMsg(ssMsg);
				}
			}
		}
	}

	// 0-uguale; 2-input; 1-output;3-clienti
	private int isEqualServerList(ArrayList<ServerParameters> list1) {
		ArrayList<ServerParameters> list0 = getMyServerList();
		int r = 0;
		if (list0.size() != list1.size()) {
			if (!equalServersAddressPort(myInputServerFromList(list1), myInputServer())) {
				return 2;
			} else if (!equalServersAddressPort(myOutputServerFromList(list1),
					myOutputServer())) {
				return 1;
			}
			r = -1;
		} else {
			for (int i = 0; i < list0.size(); i++) {
				if (list0.get(i).getNumberOfClients() != list1.get(i)
						.getNumberOfClients()) {
					r = 3;
				}
			}
		}
		return r;
	}

	private ServerParameters myInputServerFromList(ArrayList<ServerParameters> list) {
		int i = 1;
		for (ServerParameters serverParameters : list) {
			if (serverParameters.getAddress().equalsIgnoreCase(
					this.getMyServerParameters().getAddress())
					&& serverParameters.getPort() == this.getMyServerParameters()
							.getPort()) {
				if (i == list.size()) {
					return list.get(0);
				}
				return list.get(i);
			}
			i++;
		}
		return null;
	}

	private ServerParameters myInputServer() {
		if (myServerList.size() == 1) {
			return null;
		}
		int ringInNumber;
		if (myServerParameters.getRingNumber() == myServerList.size() - 1) {
			ringInNumber = 0;
		} else {
			ringInNumber = myServerParameters.getRingNumber() + 1;
		}
		int i = 1;
		for (ServerParameters serverParameters : this.getMyServerList()) {
			if (equalServersAddressPort(serverParameters, myServerParameters)) {
				break;
			}
			i++;
		}

		if (ringInNumber != i % myServerList.size()) {
			System.out.println("errore di ricerca Input Server");
		}
		return myServerList.get(ringInNumber);
	}

	private ServerParameters myOutputServerFromList(ArrayList<ServerParameters> list) {
		ServerParameters temp = null;
		for (ServerParameters serverParameters : list) {
			if (serverParameters.getAddress().equalsIgnoreCase(
					this.getMyServerParameters().getAddress())
					&& serverParameters.getPort() == this.getMyServerParameters()
							.getPort()) {
				if (temp == null) {
					return list.get(list.size() - 1);
				} else {
					return temp;
				}
			}
			temp = serverParameters;
		}
		return null;
	}

	private ServerParameters myOutputServer() {
		if (getMyServerList().size() == 1) {
			return null;
		}

		ServerParameters tmp = null;
		for (ServerParameters parameters : myServerList) {
			if (equalServersAddressPort(parameters, getMyServerParameters())) {
				if (tmp == null) {
					tmp = myServerList.get(myServerList.size() - 1);
				} else {
					break;
				}
			}
			tmp = parameters;
		}
		return tmp;
	}

	public void disconnectOutServer() {
		outServerThread.disconnect();
		// outServerAddress = null;
		System.out.println(outServerAddress + "//////" + outServerPort);
		removeOutServer();
		if (myServerList.size() > 1) {
			System.out.println(outServerAddress + "//////" + outServerPort);
			connectToAnotherServer(outServerAddress, outServerPort, true);
		}
	}

	public void removeOutServer() {
		ArrayList<ServerParameters> tmp = new ArrayList<ServerParameters>();
		int i = 0;
		for (ServerParameters parameters : myServerList) {
			if (!(parameters.getAddress().equalsIgnoreCase(outServerAddress) && parameters
					.getPort() == outServerPort)) {
				parameters.setRingNumber(i);
				tmp.add(parameters);
				i++;
			}
		}
		setMyServerList(tmp);
		if (tmp.size() > 1) {
			outServerAddress = myOutputServer().getAddress();
			outServerPort = myOutputServer().getPort();
		} else {
			outServerThread = null;
			outServerAddress = null;
			outServerPort = -1;
			broadcast(myServerList);
		}
	}
}
